# 网络TCP net
30 秒速记
net模块 =Node里的TCP/IPC编程接口,http就搭在它上面TCP是可靠、有序的字节流,没有「消息」的概念 → 一次data不等于一条消息- 拆包、粘包要应用层自己定格式:按换行分隔,或者「长度头 + 内容」
- 写数据要看背压(
write()返回false就等drain),还要处理超时、ECONNRESET、半关闭 - 解析器要限制单帧最大长度,否则对方发个超大长度头就能把内存吃满
net 是 Node 做 TCP 编程的底层模块,理解它的关键是记住 TCP 给你的是一条字节流,不是一条条消息。 打个比方,TCP 像一根水管,你分三次倒进去三杯水,对面接到的可能是一大桶,也可能是半杯半杯的,杯子的边界没了。所以对方一次 data 事件拿到的数据,可能是半条消息,也可能是两条半,必须自己约定分隔方式。另外写得太快对方收不过来时,数据会堆在内存里,这就是背压问题。真正写 TCP 服务,这两件事加上错误和超时处理才是主要工作量。
// 按换行分帧:处理粘包 / 拆包
let buf = '';
socket.setEncoding('utf8');
socket.on('data', chunk => {
buf += chunk;
let i;
while ((i = buf.indexOf('\n')) !== -1) {
const msg = buf.slice(0, i); // 一条完整消息
buf = buf.slice(i + 1);
console.log('收到消息:', msg);
}
if (buf.length > 64 * 1024) socket.destroy(); // 防恶意超长
});
理解「网络TCP net」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 network 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
TCP 编程最容易被忽视的是「消息边界」。TCP 在传输层只保证两件事:字节不丢、顺序不乱。至于你调用了几次 write,对面就按几次收到,这个 TCP 完全不管。发送端内核可能把几次小写入合并成一个包(Nagle 算法会主动这么做),一次大写入也可能被切成好几个包,接收端每次 data 拿到的就是这一刻缓冲区里有的字节。
所以几乎所有基于 TCP 的协议都自带分帧规则。常见的有两种。一种是分隔符,比如 Redis 协议和很多文本协议用 \r\n,实现简单,但内容里不能出现分隔符,或者要转义。另一种是长度前缀,先发固定字节数的长度,再发内容,二进制协议基本都这么做,解析时攒够长度头读出长度,再攒够这么多字节才算一条。
第二件事是背压。socket.write() 返回 true 表示数据已经交给内核或者缓冲还没满,返回 false 表示用户态缓冲已经超过 highWaterMark,这时候还接着写,数据只会越堆越多。正确做法是等 drain 事件再继续,或者用 stream.pipeline() 让它自动处理。
第三件事是异常路径。ECONNRESET、EPIPE、超时、对端半关闭在公网上都是常态,每个 socket 都要监听 error,不然一条连接出错就能让整个进程退出。
面试官追问
追问 1客户端连续 write 了两次 'hello',服务端一次 data 就收到了 'hellohello',是出错了吗?
不是,这就是粘包。TCP 只保证字节顺序,不保证你的写入边界,内核会合并发送。应用层要自己分帧,比如每条消息后面加 \n,或者前 4 个字节写长度。
追问 2那「长度头」方案具体怎么做?
发送方先写一个 4 字节的长度再写内容:buf.writeUInt32BE(body.length);接收方攒够 4 字节读出长度,再攒够这么多字节才算一条完整消息,剩下的留给下一轮。长度要设上限,超过直接断开。
追问 3给慢客户端狂 write,进程内存一路涨,怎么回事?
write() 返回 false 时数据已经堆在用户态缓冲里了,你还在继续写。要停下来等 drain 事件再写,或者直接用 stream.pipeline(source, socket),它会自动处理背压。
追问 4线上偶尔出现 Error: read ECONNRESET 把进程搞崩了,怎么处理?
对方粗暴断开(发了 RST)时 socket 会触发 error,没监听 error 的话会变成未捕获异常直接崩进程。每个 socket 都要 socket.on('error', ...),记日志后清理掉这条连接就行,这种错误在公网上很正常。
追问 5自己写 TCP 协议和直接用 WebSocket / HTTP,怎么选?
能用 HTTP 或 WebSocket 就别自己造,分帧、心跳、代理穿透都有现成方案。自定义 TCP 协议适合对接已有设备协议、Redis 这类自带协议的服务,或者对延迟和包大小特别敏感的场景。
# 模块概览
30 秒速记
net是核心模块,两个主角:net.Server(监听、接连接)和net.Socket(一条连接)http.Server继承自net.Server,每个HTTP请求底下都是一个net.Socketnet.Socket是双工流(Duplex),既能读也能写,pipe那一套都能用- 同一套
API既能走TCP(host+port),也能走本地IPC(Unix socket或Windows命名管道) - 写
Node服务端绕不开它:连接数、超时、keep-alive这些问题最后都落在socket上
net 模块就两样东西:net.Server 负责在端口上等连接,net.Socket 代表一条已经建立的连接。 我们平时写的 http.createServer 其实就是在 net.Server 上面加了一层 HTTP 解析,req.socket 拿到的就是底下那个 net.Socket。Socket 是个双工流,一头读一头写,所以能直接 pipe。另外它不只支持 TCP,传一个路径进去就变成本机进程间通信,Docker 的 /var/run/docker.sock 就是这种。
const http = require('http');
const net = require('net');
const server = http.createServer((req, res) => {
console.log(req.socket instanceof net.Socket); // true
res.end('ok');
});
console.log(server instanceof net.Server); // true
net模块是同样是nodejs的核心模块。在http模块概览里提到,http.Server继承了net.Server,此外,http客户端与http服务端的通信均依赖于socket(net.Socket)。也就是说,做node服务端编程,net基本是绕不开的一个模块。
从组成来看,net模块主要包含两部分,了解socket编程的同学应该比较熟悉了:
- net.Server:TCP server,内部通过socket来实现与客户端的通信。
- net.Socket:tcp/本地 socket的node版实现,它实现了全双工的stream接口。
本文从一个简单的 tcp服务端/客户端 的例子开始讲解,好让读者有个概要的认识。接着再分别介绍 net.Server、net.Socket 比较重要的API、属性、事件。
对于初学者,建议把文中的例子本地跑一遍加深理解。
面试官追问
追问 1有人说 HTTP 服务和 TCP 服务是两回事,HTTP 不用管 socket,对吗?
不对,http.Server 就是 net.Server 的子类。HTTP 只是跑在 TCP 连接上的一套文本格式,连接超时、keep-alive、连接数上限这些问题,最后都要在 socket 层面处理。
追问 2HTTP 服务连接数一直涨不下来,路由日志看着都正常,该看哪?
看连接层。用 server.getConnections(cb) 看当前连接数,ss -tnp 看 ESTABLISHED 的连接是谁的。常见原因是客户端开着 keep-alive 一直不关,或者有请求没 res.end(),socket 一直挂着。
追问 3本机两个进程通信,用 TCP 端口还是 Unix socket?
只在本机的话 Unix socket 更合适,不占端口、不经过网络协议栈、还能用文件权限控制谁能连。net.createServer().listen('/tmp/app.sock') 就行,代码和 TCP 版基本一样。
追问 4要做一个设备上报的纯 TCP 服务,能套 Express 吗?
套不了,Express 只认 HTTP。设备发的是自定义二进制协议,就直接 net.createServer,在 socket 上自己做分帧和解析,这部分没框架能帮你。
# 简单的 server+client 例子
30 秒速记
